﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Text;
using System.Xml;
using System.Xml.Linq;

using MASAS.Common.Geometry;

namespace MASAS.MSM.DomainLayer.Model
{

    /// <summary>
    /// MASAS Entry
    /// </summary>
    public class HubEntry
    {

        /// <summary>
        /// Gets or sets the identifier.
        /// </summary>
        /// <value>
        /// The identifier.
        /// </value>
        public Guid Identifier { get; private set; }

        /// <summary>
        /// Gets or sets the external identifier.
        /// </summary>
        /// <value>
        /// The external identifier.
        /// </value>
        public string ExternalIdentifier { get; set; }

        /// <summary>
        /// Gets or sets the author.
        /// </summary>
        /// <value>
        /// The author.
        /// </value>
        public EntryAuthor Author { get; set; }
        
        /// <summary>
        /// Gets or sets the title.
        /// </summary>
        /// <value>
        /// The title.
        /// </value>
        public string Title { get; set; }

        /// <summary>
        /// Gets or sets the content.
        /// </summary>
        /// <value>
        /// The content.
        /// </value>
        public string Content { get; set; }

        /// <summary>
        /// Gets or sets the summary.
        /// </summary>
        /// <value>
        /// The summary.
        /// </value>
        public string Summary { get; set; }

        /// <summary>
        /// Gets or sets the published date/time.
        /// </summary>
        /// <value>
        /// The published date/time.
        /// </value>
        public DateTime Published { get; set; }

        /// <summary>
        /// Gets or sets the last updated date/time.
        /// </summary>
        /// <value>
        /// The last updated date/time.
        /// </value>
        public DateTime LastUpdated { get; set; }

        /// <summary>
        /// Gets or sets the expiration date/time.
        /// </summary>
        /// <value>
        /// The expiration date/time.
        /// </value>
        public DateTime Expiration { get; set; }

        /// <summary>
        /// Gets or sets the geographic location of this entry.
        /// </summary>
        /// <value>
        /// The geographic location of this entry.
        /// </value>
        public GeoPoint Location { get; set; }

        /// <summary>
        /// Gets or sets the categories.
        /// </summary>
        /// <value>
        /// The categories.
        /// </value>
        public List< CategoryTypes > Categories { get; set; }

        /// <summary>
        /// Gets or sets the status.
        /// </summary>
        /// <value>
        /// The status.
        /// </value>
        public StatusTypes Status { get; set; }

        /// <summary>
        /// Gets or sets the severities.
        /// </summary>
        /// <value>
        /// The severities.
        /// </value>
        public List< SeverityTypes > Severities { get; set; }
        
        /// <summary>
        /// Gets or sets the certainties.
        /// </summary>
        /// <value>
        /// The certainties.
        /// </value>
        public List< CertaintyTypes > Certainties { get; set; }

        /// <summary>
        /// Gets or sets the symbol.
        /// </summary>
        /// <value>
        /// The symbol.
        /// </value>
        public string Symbol { get; set; }

        /// <summary>
        /// Gets or sets the digest.
        /// DIGEST - a controlled description based on the contents of the BasicEntry and its derivatives.
        /// </summary>
        /// <value>
        /// The digest.
        /// </value>
        public string Digest { get; set; }

        /// <summary>
        /// Gets or sets the (ATOM) feed entry XML.
        /// </summary>
        /// <value>
        /// The (ATOM) feed entry XML.
        /// </value>
        public string FeedEntryXML { get; set; }
        
        /// <summary>
        /// Gets or sets the (CAP) alert XML.
        /// </summary>
        /// <value>
        /// The (CAP) alert XML.
        /// </value>
        public string AlertXML { get; set; }

        /// <summary>
        /// Gets or sets the state of the item based on filtering.
        /// </summary>
        /// <value>
        /// The state of the item based on filtering.
        /// </value>
        public FilterStateType FilterState { get; set; }

        /// <summary>
        /// Gets or sets the attachments.
        /// </summary>
        /// <value>
        /// The attachments.
        /// </value>
        public List<EntryAttachment> Attachments { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="HubEntry"/> class.
        /// </summary>
        public HubEntry()
        {
            Identifier  = Guid.NewGuid();
            Author      = new EntryAuthor();
            Categories  = new List<CategoryTypes>();
            Severities  = new List<SeverityTypes>();
            Certainties = new List<CertaintyTypes>();
            FilterState = FilterStateType.None;
            Attachments = new List<EntryAttachment>();
        }

        /// <summary>
        /// Creates a <see cref="HubEntry"/> using a <see cref="SyndicationItem"/>.
        /// </summary>
        /// <param name="entry">The entry.</param>
        /// <returns>A newly created <see cref="HubEntry"/></returns>
        internal static HubEntry Create( SyndicationItem entry )
        {
            HubEntry hubEntry = new HubEntry();

            // Extract the id...
            string[] idValues = entry.Id.Split(':');
            hubEntry.ExternalIdentifier = idValues[ idValues.Length - 1 ];

            // Extract the rest of the item...
            hubEntry.Author.Name        = entry.Authors[0].Name;
            hubEntry.Author.Uri         = entry.Authors[0].Uri;
            hubEntry.Author.Email       = entry.Authors[0].Email;
            hubEntry.Title              = GetText( entry.Title );
            hubEntry.Content            = GetText( entry.Content );
            hubEntry.Digest             = GetText( entry.Content );   // DEO 28FEB2012 - putting good text here.

            hubEntry.Summary            = GetText( entry.Summary );
            hubEntry.Published          = new DateTime( entry.PublishDate.DateTime.Ticks, DateTimeKind.Utc ).ToLocalTime();
            hubEntry.LastUpdated        = new DateTime( entry.LastUpdatedTime.DateTime.Ticks, DateTimeKind.Utc ).ToLocalTime();

            // Get the expires date/time...
            SyndicationElementExtension expiresExt = entry.ElementExtensions.First<SyndicationElementExtension>( x => x.OuterName == "expires" );
            if( expiresExt != null )
            {
                XElement xelement = ( XElement )XElement.ReadFrom( expiresExt.GetReader() );
                hubEntry.Expiration = XmlConvert.ToDateTime( xelement.Value, XmlDateTimeSerializationMode.Local );
            }
            
            // TODO: Convert "Calculate Point" to using the extentions instead...
            //foreach( SyndicationElementExtension extension in entry.ElementExtensions.Where<SyndicationElementExtension>( x => x.OuterNamespace == "http://www.georss.org/georss" ) )
            //{
            //    XElement xelement = ( XElement )XElement.ReadFrom( extension.GetReader() );
            //}
            //hubEntry.Location = entry.ElementExtensions;

            IEnumerable<SyndicationCategory> masasCat = entry.Categories.Where( x => x.Scheme.Contains( "masas:category" ) );
            foreach( SyndicationCategory category in masasCat )
            {
                if( category.Scheme == "masas:category:category" )
                {
                    hubEntry.Categories.Add( ( CategoryTypes )Enum.Parse( typeof( CategoryTypes ), category.Name ) );
                }
                else if( category.Scheme == "masas:category:status" )
                {
                    hubEntry.Status = ( StatusTypes )Enum.Parse( typeof( StatusTypes ), category.Name );
                }
                else if( category.Scheme == "masas:category:severity" )
                {
                    hubEntry.Severities.Add( ( SeverityTypes )Enum.Parse( typeof( SeverityTypes ), category.Name ) );
                }
                else if( category.Scheme == "masas:category:certainty" )
                {
                    hubEntry.Certainties.Add( ( CertaintyTypes )Enum.Parse( typeof( CertaintyTypes ), category.Name ) );
                }
                else if( category.Scheme == "masas:category:icon" )
                {
                    hubEntry.Symbol = MasasService.Instance.SymbolManager.ValidateSymbolKey( category.Name );
                }
            }

            // Attachments...
            IEnumerable<SyndicationLink> masasLinks = entry.Links.Where( x => x.RelationshipType.Contains( "enclosure" ) );
            foreach( SyndicationLink link in masasLinks )
            {
                EntryAttachment attachment = new EntryAttachment();

                attachment.ContentType  = link.MediaType;
                attachment.Length       = link.Length;
                attachment.HRef         = link.Uri;
                attachment.Title        = link.Title;

                hubEntry.Attachments.Add( attachment );
            }

            hubEntry.FeedEntryXML = SaveAsAtom( entry );

            // TODO: Remove this for a better solution (as mentioned above)...
            CalculatePoint( hubEntry );

            return hubEntry;
        }

        /// <summary>
        /// Gets the text found within the syndication content.
        /// </summary>
        /// <param name="syndicationContent">Content containing the text.</param>
        /// <returns>The text.</returns>
        private static string GetText( SyndicationContent syndicationContent )
        {
            string retValue = "";

            // We need a text syndication content...
            if( syndicationContent is TextSyndicationContent )
            {
                TextSyndicationContent textContent = ( TextSyndicationContent )syndicationContent;

                if( textContent.Type == "text" )
                {
                    // Simple: we have the text...
                    retValue = textContent.Text;
                }
                else if( textContent.Type == "xhtml" )
                {
                    // More complicated: we need to extract the text from the appropriate <div>...
                    XmlDocument xmlDoc = new XmlDocument();
                    xmlDoc.LoadXml( textContent.Text );
                    XmlNamespaceManager nsMgr = new XmlNamespaceManager( xmlDoc.NameTable );
                    nsMgr.AddNamespace( "tmp", "http://www.w3.org/1999/xhtml" );

                    XmlElement root = xmlDoc.DocumentElement;
                    XmlNodeList nodes = root.SelectNodes( "//tmp:div[@xml:lang]", nsMgr );
                    if( nodes != null && nodes.Count > 0 )
                    {
                        retValue = nodes[0].InnerText;
                    }
                }
            }
            
            return retValue;
        }

        /// <summary>
        /// Saves the syndication item as an ATOM xml string.
        /// </summary>
        /// <param name="item">The syndication item.</param>
        /// <returns>XML text string.</returns>
        private static string SaveAsAtom( System.ServiceModel.Syndication.SyndicationItem item )
        {
            StringBuilder strBuilder = new StringBuilder();
            XmlWriter xmlWriter = XmlWriter.Create( strBuilder );
            item.SaveAsAtom10( xmlWriter );
            xmlWriter.Close();

            return strBuilder.ToString();
        }

        /// <summary>
        /// Calculates the point.
        /// </summary>
        /// <param name="masasEntry">The masas entry.</param>
        private static void CalculatePoint( HubEntry masasEntry )
        {
            // masasEntry.ElementExtensions
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.LoadXml( masasEntry.FeedEntryXML );

            // GeoRSS namespace http://www.georss.org/georss
            XmlNamespaceManager namespaceManager = new XmlNamespaceManager( xmlDoc.NameTable );
            namespaceManager.AddNamespace( "georss", "http://www.georss.org/georss" );

            XmlNodeList nodeList;
            XmlElement root = xmlDoc.DocumentElement;
            
            // Point
            nodeList = root.SelectNodes( "//georss:point", namespaceManager );
            foreach( XmlNode geoElement in nodeList )
            {
                masasEntry.Location = GetPointFromGeoRSS( geoElement.InnerXml );
            }

            // Polygon
            nodeList = root.SelectNodes( "//georss:polygon", namespaceManager );
            foreach( XmlNode geoElement in nodeList )
            {
                //TODO: Put REAL point here - generate using centroid of polygon.
                List<GeoPoint> pointList = StringToPoints( geoElement.InnerXml );

                // take middle point of the polygon for now

                int i = pointList.Count;
                i = i / 2;

                masasEntry.Location = pointList[i];
            }

            // Line
            nodeList = root.SelectNodes( "//georss:line", namespaceManager );
            foreach( XmlNode geoElement in nodeList )
            {
                //TODO: Put REAL point here...
                List<GeoPoint> pointList = StringToPoints( geoElement.InnerXml );

                masasEntry.Location = pointList[0];
            }

            // Box
            nodeList = root.SelectNodes( "//georss:box", namespaceManager );
            foreach( XmlNode geoElement in nodeList )
            {
                //TODO: Put REAL point here...
                List<GeoPoint> pointList = StringToPoints( geoElement.InnerXml );

                masasEntry.Location = pointList[0];
            }

            // Circle
            nodeList = root.SelectNodes( "//georss:circle", namespaceManager );
            foreach( XmlNode geoElement in nodeList )
            {
                //TODO: Put REAL point here...
                List<GeoPoint> pointList = StringToPoints( geoElement.InnerXml );

                masasEntry.Location = pointList[0];
            }

        }

        /// <summary>
        /// GetPointfromGeoRSS(string georssRawText) - rips out the latitude and longitue
        /// values from the incoming .InnerXML value.
        /// ASSUMPTION: Data are in GeoRSS WGS84 (EPSG:4326), which assumes
        /// "latitude longitude" values.
        /// </summary>
        /// <param name="georssRawText">The georss raw text.</param>
        /// <returns></returns>
        private static GeoPoint GetPointFromGeoRSS( string georssRawText )
        {
            string[] splits = georssRawText.Split( new char[] { ' ' } );
            GeoPoint point = new GeoPoint();
            point.Latitude = Convert.ToDouble( splits[0] );
            point.Longitude = Convert.ToDouble( splits[1] );

            return point;
        }

        /// <summary>
        /// Strings to points.
        /// </summary>
        /// <param name="str">The STR.</param>
        /// <returns></returns>
        private static List<GeoPoint> StringToPoints( string str )
        {
            string[] sxsy = str.Split( new char[] { ' ' } );
            List<GeoPoint> pnts = new List<GeoPoint>();

            for( int i = 0; i < sxsy.Length - 1; i += 2 )
            {
                string slat = sxsy[i];
                string slong = sxsy[i + 1];
                double x = double.NaN;
                double y = double.NaN;
                if( double.TryParse( slat, out y ) &&
                  double.TryParse( slong, out x ) )
                {
                    GeoPoint newPt = new GeoPoint();
                    newPt.Latitude = y;
                    newPt.Longitude = x;

                    pnts.Add( newPt );
                }
            }
            if( pnts.Count > 0 )
                return pnts;
            else return null;
        }

    }

}
